﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.IO;
using System.Diagnostics;
using System.Linq;
using BkXitiTagger.Util;

namespace BkXitiTagger
{
    /// <summary>
    ///  Helper for statistics gathering
    ///  The singleton should be initialized during application start-up (e.g. App.xaml.cs)
    /// </summary>
    /// <remarks>Singleton</remarks>
    public class BkXitiTaggingService
    {
        #region Singleton
        private static object syncRoot = new Object();
        private static volatile BkXitiTaggingService _instance = null;

        /// <summary>
        /// Singleton
        /// </summary>
        public static BkXitiTaggingService Instance
        {
            get
            {
                if (_instance == null)
                {
                    lock (syncRoot)
                    {
                        if (_instance == null)
                            _instance = new BkXitiTaggingService();
                    }
                }
                return _instance;
            }
        }
        #endregion Singleton

        #region Constants
        private const string XitiUrlFormat = "http{0}://{1}.xiti.com/hit.xiti?s={2}{3}";
        private const string HttpsToken = "s";
        private const string SubsiteIdParameterName = "&s2={0}";
        private const string PhoneInfoParameterFormat = "&{0}={1}";
        private const string RandomNumberParameterName = "&na={0}";
        private const string TimestampParameterName = "&hl={0:HH}x{0:mm}x{0:ss}";
        private const string XitiAllowedChars = "[^a-zA-Z0-9:]";
        private const string XitiReplacementChar = "_";
        private const string UserAgentFormat = "BkXitiSeven/1.0 ({0}; {1}; {2})";

        private const string ActionNameParameterFormat = "&p={0}&clic='{1}'";
        private const string ActionNameWithParamsParameterFormat = "&p={0}&clic='{1}'{2}";
        private const string PageNameWithParamsParameterFormat = "&p={0}{1}";
        private const string PageNameParameterFormat = "&p={0}";
        #endregion Constants

        /// <summary>
        /// Stats subdomain
        /// </summary>
        public string SubDomain { get; private set; }

        /// <summary>
        /// Stats site
        /// </summary>
        public string SiteId { get; private set; }

        /// <summary>
        /// Stats subsite
        /// </summary>
        public string SubsiteId { get; private set; }

        /// <summary>
        /// Phone information map
        /// </summary>
        private Dictionary<string, string> PhoneInformationMap { get; set; }

        /// <summary>
        /// Queue used to limit parallel network communications
        /// </summary>
        private Queue<XitiTagOperation> OperationQueue { get; set; }

        /// <summary>
        /// Flag indicating current processing of queue
        /// </summary>
        private bool IsAlreadyProcessingQueue { get; set; }

        #region Behaviour
        /// <summary>
        /// Using secure statistics reporting (using HTTPS)
        /// </summary>
        public bool IsSecure { get; private set; }

        /// <summary>
        /// Use the queue to send tags sequentially
        /// </summary>
        public bool UseQueue { get; private set; }        

        ///// <summary>
        ///// Perform an automatic flush of the queue when it reaches the specified amount
        ///// </summary>
        //public int? QueueAutoFlushThreshold { get; set; }

        /// <summary>
        /// Send a custom user agent when performing the tag request
        /// </summary>
        public bool SendCustomUserAgent { get; set; }
        #endregion Behaviour

        private BkXitiTaggingService()
        {
            this.IsAlreadyProcessingQueue = false;
        }

        /// <summary>
        /// Initialize a tag with a sub domain identifier, a site identifier, a subsite identifier and security indicator
        /// </summary>
        /// <param name="subDomain">The targeted analytics subdomain</param>
        /// <param name="siteId">The targeted analytics site</param>
        /// <param name="subsiteId">The targeted analytics subsite</param>
        /// <param name="isSecure">Use HTTPS connection</param>
        /// <param name="useQueue">Use the internal queue to queue up tagging operations</param>
        public void Initialize(string subDomain, string siteId, string subsiteId, bool isSecure = false, bool useQueue = false, bool sendCustomUserAgent =false)
        {
            this.SubDomain = subDomain;
            this.SiteId = siteId;
            this.SubsiteId = subsiteId;
            this.IsSecure = isSecure;
            this.UseQueue = useQueue;
            this.SendCustomUserAgent = sendCustomUserAgent;
            this.PhoneInformationMap = new PhoneInformation().Information;
            this.OperationQueue = new Queue<XitiTagOperation>();
        }
        
        /// <summary>
        /// Replace characters unsupported by Xiti with '_'
        /// </summary>
        /// <param name="txt">The string to transform</param>
        /// <returns>The string in a format supported by XiTi</returns>
        private string ProcessText(string txt)
        {
            return txt.ReplaceRegex(BkXitiTaggingService.XitiAllowedChars, BkXitiTaggingService.XitiReplacementChar);
        }

        private void LaunchRequest(string toAppend)
        {
            if (string.IsNullOrEmpty(this.SubDomain))
                throw new ArgumentException("Parameter SubDomain must not be null or empty");
            if (string.IsNullOrEmpty(this.SiteId))
                throw new ArgumentException("Parameter SiteId must not be null or empty");

            string url = string.Format(BkXitiTaggingService.XitiUrlFormat,
                                        this.IsSecure ? BkXitiTaggingService.HttpsToken : "",
                                        this.SubDomain,
                                        this.SiteId,
                                        toAppend);

            if (!string.IsNullOrEmpty(this.SubsiteId))
            {
                url += string.Format(BkXitiTaggingService.SubsiteIdParameterName, this.SubsiteId);
            }

            // Phone information
            foreach (KeyValuePair<string, string> phoneInfo in PhoneInformationMap)
            {
                url += string.Format(BkXitiTaggingService.PhoneInfoParameterFormat, phoneInfo.Key, MiscExtensions.UrlEncodeSpacePercentage(phoneInfo.Value));
            }

            // Random number
            url += string.Format(BkXitiTaggingService.RandomNumberParameterName, DateTime.Now.Ticks);

            DateTime tStamp = DateTime.Now;
            url += string.Format(BkXitiTaggingService.TimestampParameterName, tStamp);

            // If queuing is configured, enqueue
            if (this.UseQueue)
            {
                this.OperationQueue.Enqueue(new XitiTagOperation(url));

                // if not processing queue, launch processing
                if (!this.IsAlreadyProcessingQueue)
                {
                    this.FlushQueue();
                }
                // else do nothing: the current processing of the queue will execute the operation that was just queued
            }
            else // otherwise execute right now
            {
                ThreadPool.QueueUserWorkItem((state) => SendXitiTagHttpRequest(new XitiTagOperation(url)));
            }
        }

        private void SendXitiTagHttpRequest(XitiTagOperation tagOperation)
        {
            if (tagOperation == null
                || string.IsNullOrEmpty(tagOperation.Url))
                throw new ArgumentException("Parameter tagOperation must not be empty or null");

            HttpWebRequest hwr = HttpWebRequest.CreateHttp(tagOperation.Url);
            hwr.AllowReadStreamBuffering = true;

            if (this.SendCustomUserAgent)
            {
                hwr.UserAgent = string.Format(BkXitiTaggingService.UserAgentFormat,
                                              PhoneInformationMap[PhoneInformation.ModelParameterName],
                                              PhoneInformationMap[PhoneInformation.OperationSystemParameterName],
                                              PhoneInformationMap[PhoneInformation.LanguageParameterName]);
            }

            hwr.BeginGetResponse((asynchronousResult) =>
                {
                    HttpWebRequest request = (HttpWebRequest)asynchronousResult.AsyncState;

                    try
                    {
                        HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asynchronousResult);

                        string replyContent;
                        using (StreamReader streamReader = new StreamReader(response.GetResponseStream()))
                        {
                            string resultString = streamReader.ReadToEnd();

                            replyContent = resultString;
                            Debug.WriteLine("XiTi Tag Request Successful");
                        }
                    }
                    catch (WebException we)
                    {
                        if (we.Status == WebExceptionStatus.RequestCanceled)
                        {
                            Debug.WriteLine("XiTi Tag Request Canceled");
                        }
                        else
                        {
                            Debug.WriteLine("XiTi Tag Request failed " + we.Status + " (" + we.Message + ")");
                        }
                    }
                    finally
                    {
                        this.DequeueAndExecute();
                    }
                },
                hwr);
        }

        /// <summary>
        /// Tag a page with a name
        /// </summary>
        /// <param name="pageName">Name of the page to be executed</param>
        public void TagPage(string pageName)
        {
            if (string.IsNullOrEmpty(pageName))
                throw new ArgumentException("Parameter pageName must not be empty nor null");

            string xitiPageName = this.ProcessText(pageName);
            this.LaunchRequest(string.Format(BkXitiTaggingService.PageNameParameterFormat, xitiPageName));
        }

        /// <summary>
        /// Tag a page with a name and a sub site
        /// </summary>
        /// <param name="pageName">Name of the page to be executed</param>
        /// <param name="subsiteId">The targeted SubSite</param>
        public void TagPage(string pageName, string subsiteId)
        {
            if (string.IsNullOrEmpty(pageName))
                throw new ArgumentException("Parameter pageName must not be empty nor null");

            this.SubsiteId = subsiteId;
            string xitiPageName = this.ProcessText(pageName);
            this.LaunchRequest(string.Format(BkXitiTaggingService.PageNameParameterFormat, xitiPageName));
        }

        /// <summary>
        /// Tag a page with a name and the provided parameters
        /// </summary>
        /// <param name="pageName">Name of the page to be executed</param>
        /// <param name="parameters">Additional parameters</param>
        public void TagPage(string pageName, Dictionary<string, string> parameters)
        {
            if (string.IsNullOrEmpty(pageName))
                throw new ArgumentException("Parameter pageName must not be empty nor null");

            string xitiPageName = this.ProcessText(pageName);
            string strParameters = parameters.ToUrlParametersEscaped();
            string url = string.Format(BkXitiTaggingService.PageNameWithParamsParameterFormat, xitiPageName, strParameters);
            this.LaunchRequest(url);
        }

        /// <summary>
        /// Tag an action with a name and an action type
        /// </summary>
        /// <param name="actionName">Name of the action to be executed</param>
        /// <param name="actionType">Type of the action to be executed</param>
        public void TagAction(string actionName, TagActionType actionType)
        {
            string xitiActionName = this.ProcessText(actionName);

            string url = string.Format(BkXitiTaggingService.ActionNameParameterFormat, xitiActionName, actionType.GetStringEnumValue());
            this.LaunchRequest(url);
        }

        /// <summary>
        /// Tag an action with a name, an action type and a sub site id
        /// </summary>
        /// <param name="actionName">Name of the action to be executed</param>
        /// <param name="actionType">Type of the action to be executed</param>
        /// <param name="subsiteId">The targeted SubSite</param>
        public void TagAction(string actionName, TagActionType actionType, string subsiteId)
        {
            this.SubsiteId = subsiteId;
            string xitiActionName = this.ProcessText(actionName);
            string strActionType = actionType.GetStringEnumValue();
            string url = string.Format(BkXitiTaggingService.ActionNameParameterFormat, xitiActionName, strActionType);
            this.LaunchRequest(url);
        }

        /// <summary>
        /// Tag an action with a name, an action type and parameters
        /// </summary>
        /// <param name="actionName">Name of the action to be executed</param>
        /// <param name="actionType">Type of the action to be executed</param>
        /// <param name="parameters">Additional parameters</param>
        public void TagAction(string actionName, TagActionType actionType, Dictionary<string, string> parameters)
        {
            string xitiActionName = this.ProcessText(actionName);
            string strActionType = actionType.GetStringEnumValue();
            string strParameters = parameters.ToUrlParametersEscaped();
            string url = string.Format(BkXitiTaggingService.ActionNameWithParamsParameterFormat, xitiActionName, strActionType, strParameters);
            this.LaunchRequest(url);
        }

        /// <summary>
        /// Execute operations waiting in the queue sequentially
        /// </summary>
        public void FlushQueue()
        {
            if (!this.UseQueue)
                return;

            this.DequeueAndExecute();
        }

        /// <summary>
        /// Dequeue an item from the queue and execute it
        /// Once it is done executing, continue with the next queue item
        /// </summary>
        /// <remarks>Recursive function</remarks>
        private void DequeueAndExecute()
        {
            // End of recursion
            if (this.OperationQueue.Count == 0)
            {
                this.IsAlreadyProcessingQueue = false;
                return;
            }

            this.IsAlreadyProcessingQueue = true;

            XitiTagOperation tagOp = this.OperationQueue.Dequeue();

            this.SendXitiTagHttpRequest(tagOp);
        }

    }
}
